/// Copyright (c) Microsoft Corporation.  All rights reserved.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;


namespace Microsoft.VSPowerToys.Common.TaskManager.ZipLibrary
{

    /// <summary>
    /// Contains shared functions for Zip functionality
    /// </summary>
    internal static class Shared
    {
        /// <summary>
        /// Creates a string from a byte array buffer
        /// </summary>
        /// <param name="buffer">Buffer to read from</param>
        /// <param name="start">Starting position</param>
        /// <param name="maxlength">Maximum length of the string</param>
        /// <returns>String read from the buffer</returns>
        internal static string StringFromBuffer(byte[] buffer, int start, int maxlength)
        {
            int i;
            char[] charArray = new char[maxlength];
            for (i = start; (i < maxlength) && (i < buffer.Length) && (buffer[i] != 0); i++)
            {
                charArray[i] = (char)buffer[i];
            }
            return new System.String(charArray, 0, i);
        }

        /// <summary>
        /// Finds the provided signature in the stream starting from current position
        /// </summary>
        /// <param name="s">Stream to search</param>
        /// <param name="signature">Signature to look for</param>
        /// <returns>Starting index of the signature (-1 if not found)</returns>
        internal static int FindSignature(System.IO.Stream s, byte[] signature)
        {
            long originalPosition = s.Position;
            int readByte = 0;
            int sigIndex = 0;
            while ((readByte = s.ReadByte()) != -1)
            {
                if (readByte == signature[sigIndex])
                {
                    sigIndex++;
                    if (sigIndex == signature.Length)
                    {
                        s.Seek(-signature.Length, System.IO.SeekOrigin.Current);
                        return (int)s.Position - signature.Length;
                    }
                }
                else
                {
                    sigIndex = 0;
                }
            }
            s.Seek(originalPosition, System.IO.SeekOrigin.Begin);
            return -1;
        }

        /// <summary>
        /// Reads the signature at the current location of the stream
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        internal static int ReadSignature(System.IO.Stream stream)
        {
            int n = 0;
            byte[] sig = new byte[4];
            n = stream.Read(sig, 0, sig.Length);
            int signature = BitConverter.ToInt32(sig, 0);
            return signature;
        }

        internal static DateTime PackedToDateTime(Int32 packedDateTime)
        {
            Int16 packedTime = (Int16)(packedDateTime & 0x0000ffff);
            Int16 packedDate = (Int16)((packedDateTime & 0xffff0000) >> 16);

            int year = 1980 + ((packedDate & 0xFE00) >> 9);
            int month = (packedDate & 0x01E0) >> 5;
            int day = packedDate & 0x001F;


            int hour = (packedTime & 0xF800) >> 11;
            int minute = (packedTime & 0x07E0) >> 5;
            int second = packedTime & 0x001F;

            DateTime d = System.DateTime.Now;
            try { d = new System.DateTime(year, month, day, hour, minute, second, 0); }
            catch (ArgumentException)
            {
            }

            return d;
        }
    }

    /// <summary>
    /// Represents a directory entry in a Zip file.
    /// </summary>
    public class ZipDirEntry
    {

        internal const int ZipDirEntrySignature = 0x02014b50;

        private ZipDirEntry() { }

        private DateTime _LastModified;
        public DateTime LastModified
        {
            get { return _LastModified; }
        }

        private string _FileName;
        public string FileName
        {
            get { return _FileName; }
        }

        private string _Comment;
        public string Comment
        {
            get { return _Comment; }
        }

        private Int16 _VersionMadeBy;
        public Int16 VersionMadeBy
        {
            get { return _VersionMadeBy; }
        }

        private Int16 _VersionNeeded;
        public Int16 VersionNeeded
        {
            get { return _VersionNeeded; }
        }

        private Int16 _CompressionMethod;
        public Int16 CompressionMethod
        {
            get { return _CompressionMethod; }
        }

        private Int32 _CompressedSize;
        public Int32 CompressedSize
        {
            get { return _CompressedSize; }
        }

        private Int32 _UncompressedSize;
        public Int32 UncompressedSize
        {
            get { return _UncompressedSize; }
        }

        private byte[] _Extra;

        public static ZipDirEntry Read(System.IO.Stream stream)
        {
            int signature = Shared.ReadSignature(stream);
            // return null if this is not a local file header signature
            if (SignatureIsNotValid(signature))
            {
                stream.Seek(-4, System.IO.SeekOrigin.Current);
                return null;
            }

            byte[] block = new byte[42];
            int n = stream.Read(block, 0, block.Length);
            if (n != block.Length) return null;

            ZipDirEntry zde = new ZipDirEntry();
            zde._VersionMadeBy = BitConverter.ToInt16(block, 0);
            zde._VersionNeeded = BitConverter.ToInt16(block, 2);
            zde._CompressionMethod = BitConverter.ToInt16(block, 6);
            Int32 lastModifiedDate = BitConverter.ToInt32(block, 8);
            zde._CompressedSize = BitConverter.ToInt32(block, 16);
            zde._UncompressedSize = BitConverter.ToInt32(block, 20);
            zde._LastModified = Shared.PackedToDateTime(lastModifiedDate);

            Int16 filenameLength = BitConverter.ToInt16(block, 24);
            Int16 extraFieldLength = BitConverter.ToInt16(block, 26);
            Int16 commentLength = BitConverter.ToInt16(block, 28);
            Int16 diskNumber = BitConverter.ToInt16(block, 30);
            Int16 internalFileAttrs = BitConverter.ToInt16(block, 32);
            Int32 externalFileAttrs = BitConverter.ToInt32(block, 34);
            Int32 Offset = BitConverter.ToInt32(block, 38);
            
            block = new byte[filenameLength];
            n = stream.Read(block, 0, block.Length);
            zde._FileName = Shared.StringFromBuffer(block, 0, block.Length);

            zde._Extra = new byte[extraFieldLength];
            n = stream.Read(zde._Extra, 0, zde._Extra.Length);

            block = new byte[commentLength];
            n = stream.Read(block, 0, block.Length);
            zde._Comment = Shared.StringFromBuffer(block, 0, block.Length);

            return zde;
        }

        /// <summary>
        /// Checks if signature matches to the known Zip file signature
        /// </summary>
        /// <param name="signature"></param>
        /// <returns></returns>
        private static bool SignatureIsNotValid(int signature)
        {
            return (signature != ZipDirEntrySignature);
        }

    }

    /// <summary>
    /// Represents a compressed file entry in a Zip file
    /// </summary>
    public class ZipEntry
    {

        private const int ZipEntrySignature = 0x04034b50;

        private DateTime _LastModified;
        public DateTime LastModified
        {
            get { return _LastModified; }
        }

        private string _FileName;
        public string FileName
        {
            get { return _FileName; }
        }

        private Int16 _VersionNeeded;
        public Int16 VersionNeeded
        {
            get { return _VersionNeeded; }
        }

        private Int16 _BitField;
        public Int16 BitField
        {
            get { return _BitField; }
        }

        private Int16 _CompressionMethod;
        public Int16 CompressionMethod
        {
            get { return _CompressionMethod; }
        }

        private Int32 _CompressedSize;
        public Int32 CompressedSize
        {
            get { return _CompressedSize; }
        }

        private Int32 _UncompressedSize;
        public Int32 UncompressedSize
        {
            get { return _UncompressedSize; }
        }

        private byte[] _Extra;
        private byte[] fileData;
        
        /// <summary>
        /// Reads header information for the ZipEntry from the stream provided
        /// </summary>
        /// <param name="stream">Stream containing header information.</param>
        /// <param name="entry">ZipEntry to read header for</param>
        /// <returns>true if successful.</returns>
        private static bool ReadHeader(System.IO.Stream stream, ZipEntry entry)
        {
            if (Shared.FindSignature(stream, new byte[] { 0x50, 0x4b, 0x03, 0x04 }) == -1)
            {
                return false;
            }
            int signature = Shared.ReadSignature(stream);

            // return null if this is not a local file header signature
            if (SignatureIsNotValid(signature))
            {
                stream.Seek(-4, System.IO.SeekOrigin.Current);
                return false;
            }

            byte[] block = new byte[26];
            int n = stream.Read(block, 0, block.Length);
            if (n != block.Length) return false;

            entry._VersionNeeded = BitConverter.ToInt16(block, 0);
            entry._BitField = BitConverter.ToInt16(block, 2);
            entry._CompressionMethod = BitConverter.ToInt16(block, 4);
            Int32 lastModifiedDate = BitConverter.ToInt32(block, 6);
            entry._LastModified = Shared.PackedToDateTime(lastModifiedDate);
            entry._CompressedSize = BitConverter.ToInt32(block, 14);
            entry._UncompressedSize = BitConverter.ToInt32(block, 18);
            Int16 filenameLength = BitConverter.ToInt16(block, 22);
            Int16 extraFieldLength = BitConverter.ToInt16(block, 24);
            block = new byte[filenameLength];
            n = stream.Read(block, 0, block.Length);
            entry._FileName = Shared.StringFromBuffer(block, 0, block.Length);
            entry._Extra = new byte[extraFieldLength];
            n = stream.Read(entry._Extra, 0, entry._Extra.Length);
            return true;
        }

        /// <summary>
        /// Checks if signature matches to the known Zip file signature
        /// </summary>
        /// <param name="signature"></param>
        /// <returns></returns>
        private static bool SignatureIsNotValid(int signature)
        {
            return (signature != ZipEntrySignature);
        }

        /// <summary>
        /// Reads a zip file entry header from the current location at the stream
        /// </summary>
        /// <param name="stream">Stream to read from</param>
        /// <returns>a ZipEntry object if one found at the current location</returns>
        public static ZipEntry Read(System.IO.Stream stream)
        {
            ZipEntry entry = new ZipEntry();
            if (!ReadHeader(stream, entry)) return null;

            entry.fileData = new byte[entry.CompressedSize];
            int readBytes = stream.Read(entry.fileData, 0, entry.fileData.Length);
            if (readBytes != entry.fileData.Length)
            {
                return null;
            }

            return entry;
        }

        /// <summary>
        /// Extracts the file to the specified directory
        /// </summary>
        /// <param name="baseDirectory">Directory to extract the file</param>
        public void Extract(string baseDirectory)
        {
            string TargetFile = null;
            if (baseDirectory != null)
            {
                TargetFile = System.IO.Path.Combine(baseDirectory, FileName);

                // check if a directory
                if (FileName.EndsWith("/"))
                {
                    if (!System.IO.Directory.Exists(TargetFile))
                        System.IO.Directory.CreateDirectory(TargetFile);
                    return;
                }
            }
            else throw new ArgumentNullException("baseDirectory");


            using (System.IO.MemoryStream memstream = new System.IO.MemoryStream(fileData))
            {

                System.IO.Stream input = null;
                try
                {

                    if (CompressedSize == UncompressedSize)
                    {
                        // the System.IO.Compression.DeflateStream class does not handle uncompressed data.
                        // so if an entry is not compressed, then we just translate the bytes directly.
                        input = memstream;
                    }
                    else
                    {
                        input = new System.IO.Compression.DeflateStream(memstream, System.IO.Compression.CompressionMode.Decompress);
                    }


                    if (TargetFile != null)
                    {
                        // ensure the target path exists
                        if (!System.IO.Directory.Exists(System.IO.Path.GetDirectoryName(TargetFile)))
                        {
                            System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(TargetFile));
                        }
                    }


                    System.IO.Stream output = null;
                    try
                    {
                        if (TargetFile != null)
                            output = new System.IO.FileStream(TargetFile, System.IO.FileMode.CreateNew);

                        byte[] bytes = new byte[4096];
                        int n;
                        n = 1; // anything non-zero
                        while (n != 0)
                        {
                            n = input.Read(bytes, 0, bytes.Length);
                            if (n > 0)
                            {
                                output.Write(bytes, 0, n);
                            }
                        }
                    }
                    finally
                    {
                        // we only close the output stream if we opened it. 
                        if ((output != null) && (TargetFile != null))
                        {
                            output.Close();
                        }
                    }

                    if (TargetFile != null)
                    {
                        // We may have to adjust the last modified time to compensate
                        // for differences in how the .NET Base Class Library deals
                        // with daylight saving time (DST) versus how the Windows
                        // filesystem deals with daylight saving time.
                        if (LastModified.IsDaylightSavingTime())
                        {
                            DateTime AdjustedLastModified = LastModified + new System.TimeSpan(1, 0, 0);
                            System.IO.File.SetLastWriteTime(TargetFile, AdjustedLastModified);
                        }
                        else
                            System.IO.File.SetLastWriteTime(TargetFile, LastModified);
                    }

                }
                finally
                {
                    // we cannot use using() here because in some cases we do not want to Dispose the stream
                    if ((input != null) && (input != memstream))
                    {
                        input.Close();
                    }
                }
            }
        }


    }

    /// <summary>
    /// Represents a zip file containing multiple file entries
    /// </summary>
    /// <remarks>Suppressed the naming message since this is not a generic ZipEntry collection</remarks>
    [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix")]
    public class ZipFile : System.Collections.Generic.IEnumerable<ZipEntry>, IDisposable
    {

        private System.IO.Stream _readstream;
        private System.Collections.Generic.List<ZipEntry> _entries = null;
        private System.Collections.Generic.List<ZipDirEntry> _direntries = null;

        private string _name;
        public string Name
        {
            get { return _name; }
        }

        private string _comment;

        /// <summary>
        /// Gets the ZIP file comment if available
        /// </summary>
        public string Comment
        {
            get { return this._comment; }
        }

        private System.IO.Stream ReadStream
        {
            get
            {
                if (_readstream == null)
                {
                    _readstream = System.IO.File.OpenRead(_name);
                }
                return _readstream;
            }
        }

        private ZipFile() { }

        #region For Reading Zip Files

        /// <summary>
        /// This will throw if the zipfile does not exist. 
        /// </summary>
        public static ZipFile Read(string fileName)
        {
            ZipFile zf = new ZipFile();
            zf._name = fileName;
            zf._entries = new System.Collections.Generic.List<ZipEntry>();
            ZipEntry e;
            while ((e = ZipEntry.Read(zf.ReadStream)) != null)
            {
                zf._entries.Add(e);
            }

            if (zf._entries.Count == 0)
            {
                return null;
            }

            // read the zipfile's central directory structure here.
            zf._direntries = new System.Collections.Generic.List<ZipDirEntry>();

            ZipDirEntry de;
            while ((de = ZipDirEntry.Read(zf.ReadStream)) != null)
            {
                zf._direntries.Add(de);
            }

            zf._comment = ReadFileComment(zf.ReadStream);

            return zf;
        }

        /// <summary>
        /// Tries to read zip file comment, stream must be located at the beginning of
        /// "Central Directory Record footer"
        /// </summary>
        /// <param name="s"></param>
        /// <returns></returns>
        internal static string ReadFileComment(System.IO.Stream stream)
        {
            string fileComment = String.Empty;
            int footerSignature = 0x06054b50;
            // return null if this is not a local file header signature
            if (Shared.ReadSignature(stream) != footerSignature)
            {
                stream.Seek(-4, System.IO.SeekOrigin.Current);
                Debug.WriteLine(
                    String.Format(System.Globalization.CultureInfo.CurrentCulture,
                    "  ZipDirEntry::Read(): Bad signature ({0:X8}) at position {1}", footerSignature, stream.Position));
                return String.Empty;
            }

            byte[] block = new byte[18];
            int n = stream.Read(block, 0, block.Length);
            if (n != block.Length) return null;

            Int16 commentLength = BitConverter.ToInt16(block, 16);
            if (commentLength > 0)
            {
                block = new byte[commentLength];
                n = stream.Read(block, 0, block.Length);
                fileComment = Shared.StringFromBuffer(block, 0, block.Length);
            }
            return fileComment;
        }

        public System.Collections.Generic.IEnumerator<ZipEntry> GetEnumerator()
        {
            foreach (ZipEntry e in _entries)
                yield return e;
        }

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        /// <summary>
        /// Extracts all files included in the archive to the specified directory
        /// </summary>
        /// <param name="path">Path to extract to</param>
        public void ExtractAll(string path)
        {
            foreach (ZipEntry e in _entries)
            {
                e.Extract(path);
            }
        }

        #endregion

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }


        protected virtual void Dispose(bool disposeManagedResources)
        {
            if (disposeManagedResources)
             {
                    // dispose managed resources
                    if (_readstream != null)
                    {
                        _readstream.Dispose();
                        _readstream = null;
                    }
            }
        }
    }
}